// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    /// <summary>
    ///     The abstract base class of a SqlBuilder object.
    /// </summary>
    public abstract class SqlBuilderBase : ISqlBuilder
    {
        protected SqlSyntaxDescription _description;
        protected int _version = -1;
        protected Dictionary<string, ISqlParameter> _usedParameters;
        protected DatabaseType _dbType = DatabaseType.SqlServer;

        /// <summary>
        ///     Initializes a new instance of the SqlBuilderBase class.
        /// </summary>
        /// <param name="name">The name specified for the new ISqlbuilder object.</param>
        public SqlBuilderBase(string name)
        {
            this.Name = name;
            _usedParameters = new Dictionary<string, ISqlParameter>(StringComparer.OrdinalIgnoreCase);
        }

        /// <summary>
        ///     Gets the name of the current object.
        /// </summary>
        public string Name { get; }

        /// <summary>
        ///     Gets the SQL statement type of the current object.
        /// </summary>
        public abstract SqlStatementType SqlType { get; }

        internal abstract SqlBuildStatement Statement { get; }

        internal ISqlParameter[] BuilderParameters => this.Statement.Parameters;

        /// <summary>
        ///     Gets an IReadOnlyDictionary<string, ISqlParameter> object that contains all parameters used in the current object.
        /// </summary>
        public IReadOnlyDictionary<string, ISqlParameter> UsedParameters => _usedParameters;

        /// <summary>
        ///     Adds the parameters which will be used in the SQL statement to the current object.
        /// </summary>
        /// <param name="parameters">
        ///     One or more SQL parameters to be used in the SQL statement. You can use the SqlBuilder.Parameter 
        ///     method to create a SQL parameter.
        /// </param>
        /// <returns>
        ///     Returns false if the parameter name is not specified or the parameter name already exists; otherwise, returns true.
        /// </returns>
        public bool AddParameters(params ISqlParameter[] parameters)
        {
            return this.Statement.AddParameters(parameters);
        }

        /// <summary>
        ///     Gets the placeholder string for a SQL parameter.
        /// </summary>
        /// <param name="name">The name of the SQL parameter to get the placeholder string.</param>
        /// <returns>
        ///     Returns the placeholder string for the SQL parameter if the parameter name exists in the current object.
        /// </returns>
        public string GetParameterPlacehold(string name)
        {
            return this.Statement.GetParameterPlacehold(name);
        }

        /// <summary>
        ///     Clears the SQL statement and all of the SQL parameters defined in the current object.
        /// </summary>
        /// <returns>Returns the current object.</returns>
        public ISqlBuilder Reset()
        {
            _usedParameters.Clear();
            this.Statement.Clear();

            _version = -1;

            return this;
        }

        /// <summary>
        ///     Gets a read-only ISqlReadOnlyBuilder object generated by the clone of the current object.
        /// </summary>
        /// <returns>
        ///     A read-only ISqlReadOnlyBuilder object which can generate the same SQL statements as the current object.
        /// </returns>
        public ISqlReadOnlyBuilder ToReadOnly()
        {
            return new SqlReadOnlyBuilder(this.Name, this.SqlType, this.Statement.Clone());
        }

        /// <summary>
        ///     Returns the raw SQL string for the corresponding database by the current object.
        /// </summary>
        /// <param name="context">
        ///     A DataContext object which contains the database connection information.
        /// </param>
        /// <returns>Returns the raw SQL string.</returns>
        public virtual string ToSqlString(DataContext context)
        {
            if (this.Statement.Version != _version ||
                context.ContextOptions.DatabaseType != _dbType)
            {
                var generator = context.AdoDbSqlProvider.SqlStatementFactory.GetBuilderGenerator(this.SqlType);

                _description = generator.Create(this.Statement);

                if (_description.Parameters.Count > 0)
                {
                    _usedParameters = this.Statement.Parameters
                        .Where(m => _description.Parameters.ContainsKey(m.Name))
                        .ToDictionary(m => m.Name, m => (ISqlParameter)m, StringComparer.OrdinalIgnoreCase);
                }
                else
                {
                    _usedParameters = this.Statement.Parameters
                        .ToDictionary(m => m.Name, m => (ISqlParameter)m, StringComparer.OrdinalIgnoreCase);
                }

                _version = this.Statement.Version;
                _dbType = context.ContextOptions.DatabaseType;
            }

            return _description.SqlText;
        }

        /// <summary>
        ///     Validates the raw SQL string for the specified database generated by the current sqlbuilder.
        /// </summary>
        /// <param name="context">A DataContext object which contains the database connection information.</param>
        /// <param name="throwError">
        ///     True (default ) -- to throw an exception if the raw SQL is not valid in the specified database;
        ///     False -- Not to throw an exception if the raw SQL is not valid in the specified database.
        /// </param>
        /// <returns>
        ///     Returns true if the raw SQL is valid in the specified database; false if the raw SQL is not valid in the specified database.
        /// </returns>
        public virtual bool Validate(DataContext context, bool throwError = true)
        {
            if (throwError)
            {
                return this.OnValidation(context);
            }
            else
            {
                try
                {
                    return this.OnValidation(context);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex);
                    return false;
                }
            }
        }

        public virtual Task<bool> ValidateAsync(DataContext context, bool throwError = true)
        {
            return this.ValidateAsync(context, throwError, default);
        }

        public virtual async Task<bool> ValidateAsync(
            DataContext context, bool throwError, CancellationToken cancellationToken = default)
        {
            if (throwError)
            {
                return await this.OnValidationAsync(context, cancellationToken);
            }
            else
            {
                try
                {
                    return await this.OnValidationAsync(context, cancellationToken);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex);

                    return false;
                }
            }
        }

        private bool OnValidation(DataContext context)
        {
            this.ToSqlString(context);

            var parameters = _usedParameters.Select(
                m => new AdoDbParameter(
                    ((ISqlBuilderParameter)m.Value).ParameterName,
                    m.Value.DataType.DefaultValue(),
                    m.Value.DataType)).ToList();

            var provider = context.AdoDbProviderFactory.GetAdoDbProvider();

            context.CurrentConnection.Open();
            try
            {
                provider.ExecuteSqlValidation(_description.SqlText, parameters);
            }
            finally
            {
                context.CurrentConnection.Close();
            }

            return true;
        }

        private async Task<bool> OnValidationAsync(
            DataContext context, CancellationToken cancellationToken = default)
        {
            this.ToSqlString(context);

            var parameters = _usedParameters.Select(
                m => new AdoDbParameter(
                    ((ISqlBuilderParameter)m.Value).ParameterName,
                    m.Value.DataType.DefaultValue(),
                    m.Value.DataType)).ToList();

            var provider = context.AdoDbProviderFactory.GetAdoDbProvider();

            await context.CurrentConnection.OpenAsync(cancellationToken);

            try
            {
                await provider.ExecuteSqlValidationAsync(
                    _description.SqlText, parameters, cancellationToken);
            }
            finally
            {
                await context.CurrentConnection.CloseAsync();
            }

            return true;
        }

        public abstract ISqlBuilder Clone();
    }
}
